<?php
/**
 * Contains implementation of mpUrlInfo.
 *
 * @package     mpUrlInfo
 * @author      Murat Purc <murat@purc.de>
 * @copyright   Copyright (c) 2008-2010 Murat Purc (http://www.purc.de)
 * @license     http://www.gnu.org/licenses/gpl-2.0.html - GNU General Public License, version 2
 * @version     $Id: mpUrlInfo.php 4 2010-02-18 22:12:06Z purcaholic $
 */


/**
 * Include interface class
 */
require_once('mpUrlInterface.php');


/**
 * Base URL class, used to represent a URL.
 *
 * Usage:
 * <code>
 * // create a url object from a existing url
 * $url = new mpUrlInfo('/path/to/page.php?param=123&other_param=12');
 * // get path '/path/to/page.php'
 * echo $url->getPath();
 * // get query 'param=123&other_param=1'
 * echo $url->getQuery();
 *
 * // create a url object from scratch
 * $url = new mpUrlInfo();
 * // set path
 * echo $url->setPath('front_content.php');
 * // set query params 'param=123&other_param=1'
 * echo $url->setParam('param', 123);
 * echo $url->setParam('other_param', 1);
 * // build url
 * echo $url
 * </code>
 *
 * @package     mpUrlInfo
 * @author      Murat Purc <murat@purc.de>
 */
class mpUrlInfo implements mpUrlInterface
{
    /**
     * Query arguments separator (generally '&' for HTML or '&amp;' for XHTML)
     *
     * @var  string
     */
    protected static $_argSeparator = '&amp;';

    /**
     * Flag to auto encode passed url properties like query values path items, etc.
     *
     * Uses urlencode() to encode those properties.
     *
     * @var  bool
     */
    protected static $_autoEncode = false;

    /**
     * List of supported URL schemes
     *
     * @var  array
     */
    protected $_supportedSchemes = array('http', 'https', 'ftp');

    /**
     * Assoziative URL components array
     *
     * @var  array
     */
    protected $_urlComp = array();

    /**
     * Assoziative array of query parameters
     *
     * @var  array
     */
    protected $_queryParams = array();


    /**
     * Initializes url object with passed url string
     *
     * @param   string  $url  The URL
     * @return  void
     */
    public function __construct($url=null)
    {
        $url = trim($url);
        if ($url !== '') {
            $this->setUrl($url);
        }
    }


    /**
     * Returns string representation of self.
     *
     * @return  string
     */
    public function __toString()
    {
        return $this->build();
    }


    /**
     * Checks passed url object against equality with self.
     *
     * @param   mpUrlInterface  $url  Url to check
     * @return  bool
     */
    public function equals(mpUrlInterface $url)
    {
        return ($this === $url);
    }


    /**
     * Compares passed url with self, excluding the fragment component.
     *
     * @param   mpUrlInterface  $url  Url to check
     * @return  bool
     */
    public function isSameUrl(mpUrlInterface $url)
    {
        $thisUrl = (string) $this;
        if ($fragment = $this->getFragment()) {
            $thisUrl = str_replace('#' . $fragment , '', $thisUrl);
        }
        $otherUrl = (string) $url;
        if ($fragment = $url->getFragment()) {
            $otherUrl = str_replace('#' . $fragment , '', $otherUrl);
        }
        return ($thisUrl === $otherUrl);
    }


    /**
     * Sets the argument separator for query parameter.
     *
     * @param   string  $argSeparator
     * @return  void
     */
    public static function setArgSeparator($argSeparator)
    {
        self::$_argSeparator = (string) $argSeparator;
    }


    /**
     * Returns the current set argument separator.
     *
     * @return  string
     */
    public static function getArgSeparator()
    {
        return self::$_argSeparator;
    }


    /**
     * Sets the auto encode flag.
     *
     * @param   bool  $autoEncode
     * @return  void
     */
    public static function setAutoEncode($autoEncode)
    {
        self::$_autoEncode = (bool) $autoEncode;
    }


    /**
     * Returns the current set auto encode flag.
     *
     * @return  bool
     */
    public static function getAutoEncode()
    {
        return self::$_autoEncode;
    }


    /**
     * Sets the url object with passed url.
     *
     * @param   string  $url
     * @return  void
     * @throws  Exception  If Url is empty or invalid!
     */
    public function setUrl($url)
    {
        $url = trim($url);
        if ($url == '') {
            throw new Exception("Empty URL can't set!");
        }

        $this->_queryParams = array();

        $comp = @parse_url($url);
        if (is_array($comp)) {
            if (isset($comp['scheme'])) {
                $this->setScheme($comp['scheme']);
            }
            if (isset($comp['user'])) {
                $this->setUser($comp['user']);
            }
            if (isset($comp['pass'])) {
                $this->setPass($comp['pass']);
            }
            if (isset($comp['host'])) {
                $this->setHost($comp['host']);
            }
            if (isset($comp['port'])) {
                $this->setPort($comp['port']);
            }
            if (isset($comp['path'])) {
                $this->setPath($comp['path']);
            }
            if (isset($comp['query'])) {
                $this->setQuery($comp['query']);
            }
            if (isset($comp['fragment'])) {
                $this->setFragment($comp['fragment']);
            }
        } else {
            throw new Exception("URL '$url' seems to be invalid!");
        }
    }


    /**
     * Sets the url scheme (e. g. http, https, etc...)
     *
     * @param   string  $scheme
     * @return  void
     * @throws  Exception  If passed scheme isn't supported
     */
    public function setScheme($scheme)
    {
        if (in_array(strtolower($scheme), $this->_supportedSchemes)) {
            $this->_urlComp['scheme'] = (string) $scheme;
        } else {
            throw new Exception('Passed scheme "' . $scheme . '" not supported!');
        }
    }


    /**
     * Returns url scheme, if exists
     *
     * @return  string|null
     */
    public function getScheme()
    {
        return (isset($this->_urlComp['scheme'])) ? $this->_urlComp['scheme'] : null;
    }


    /**
     * Sets host name.
     *
     * @param   string  $host
     * @return  void
     */
    public function setHost($host)
    {
        $host = trim($host);
        if ($host == '') {
            throw new Exception('Passed host is empty!');
        }
        $this->_urlComp['host'] = $host;
    }


    /**
     * Returns host name, if exists
     *
     * @return  string|null
     */
    public function getHost()
    {
        return (isset($this->_urlComp['host'])) ? $this->_urlComp['host'] : null;
    }


    /**
     * Sets port number of host
     *
     * @param   int  $port
     * @return  void
     * @throws  Exception  If passed port is invalid
     */
    public function setPort($port)
    {
        if (preg_match('/([0-9]{2,5})/', $port)) {
            $this->_urlComp['port'] = (int) $port;
        } else {
            throw new Exception('Passed port "' . $port . '" is invalid!');
        }
    }


    /**
     * Returns port number of host, if exists
     *
     * @return  int|null
     */
    public function getPort()
    {
        return (isset($this->_urlComp['port'])) ? $this->_urlComp['port'] : null;
    }


    /**
     * Sets the user name.
     *
     * @param   string  $user
     * @return  void
     */
    public function setUser($user)
    {
        if (self::$_autoEncode) {
            $user = urlencode(urldecode($user));
        }
        $this->_urlComp['user'] = $user;
    }


    /**
     * Returns user name, if exists.
     *
     * @return  string|null
     */
    public function getUser()
    {
        return (isset($this->_urlComp['user'])) ? $this->_urlComp['user'] : null;
    }


    /**
     * Sets the user password.
     *
     * @param   string  $pass
     * @return  void
     */
    public function setPass($pass)
    {
        if (self::$_autoEncode) {
            $pass = urlencode(urldecode($pass));
        }
        $this->_urlComp['pass'] = $pass;
    }


    /**
     * Returns user password, if exists.
     *
     * @return  string|null
     */
    public function getPass()
    {
        return (isset($this->_urlComp['pass'])) ? $this->_urlComp['pass'] : null;
    }


    /**
     * Sets the path.
     *
     * @param   string  $path
     * @return  void
     */
    public function setPath($path)
    {
        $path = trim($path);
        if ($path == '' || substr($path, 0, 1) !== '/') {
            $path = '/' . $path;
        }

        $pathItems = explode('/', $path);
        foreach ($pathItems as $p => $v) {
            if (self::$_autoEncode) {
                $v = urlencode(urldecode($v));
            }
            $pathItems[$p] = $v;
        }

        $this->_urlComp['path'] = implode('/', $pathItems);
    }


    /**
     * Returns path, with filename, if filename exists.
     *
     * @return  string|null
     */
    public function getPath()
    {
        return (isset($this->_urlComp['path'])) ? $this->_urlComp['path'] : null;
    }


    /**
     * Sets filename.
     *
     * Passed empty $filename removes filename from path.
     *
     * @param   string
     * @return  void
     */
    public function setFilename($filename)
    {
        $pathItems = explode('/', $this->getPath());
        if ($filename !== '' && self::$_autoEncode) {
            $filename = urlencode(urldecode($filename));
        }
        $pathItems[count($pathItems)-1] = $filename;
        $this->setPath(implode('/', $pathItems));
    }


    /**
     * Returns filename if exists.
     *
     * @return  string|null
     */
    public function getFilename()
    {
        if ($path = $this->getPath()) {
            $pathItems = explode('/', $path);
            return $pathItems[count($pathItems)-1];
        } else {
            return null;
        }
    }


    /**
     * Sets extension.
     *
     * The extension will added to a found filename. Passed empty $extension
     * removes it from found filename.
     *
     * @param   string
     * @return  void
     */
    public function setExtension($extension)
    {
        if ($fileName = $this->getFilename()) {
            if (strpos($fileName, '.') === false) {
                $fileName .= '.';
            }
            $arr = explode('.', $fileName);
            if ($extension == '') {
                unset($arr[count($arr)-1]);
            } else {
                $arr[count($arr)-1] = $extension;
            }
            $this->setFilename(implode('.', $arr));
        }
    }


    /**
     * Returns extension, if exists.
     *
     * @return  string
     */
    public function getExtension()
    {
        $comp = pathinfo($this->getPath());
        return (isset($comp['extension']) && !empty($comp['extension'])) ? $comp['extension'] : null;
    }


    /**
     * Sets the query.
     *
     * @param   string  $query
     * @return  void
     */
    public function setQuery($query)
    {
        // cleanup n' set query
        $query = str_replace('&amp;', '&', (string) $query);
        $query = str_replace(self::$_argSeparator, '&', $query);
        $this->_urlComp['query'] = str_replace('&', self::$_argSeparator, $query);
        // update query paramater list
        parse_str($query, $this->_queryParams);
    }


    /**
     * Returns query, if exists.
     *
     * @return  string|null
     */
    public function getQuery()
    {
        return (isset($this->_urlComp['query'])) ? $this->_urlComp['query'] : null;
    }


    /**
     * Sets a query parameter and it's value.
     *
     * Examples:
     * <code>
     * // to set foo=bar
     * $url->setQueryParameter('foo', 'bar');
     *
     * // to set foo[0]=bar&foo[1]=bar2
     * $url->setQueryParameter('foo', array('bar', 'bar2'));
     * </code>
     *
     * @param   string        $parameter  Name of query parameter
     * @param   string|array  $value      Either the value or a array of values.
     * @return  void
     */
    public function setQueryParameter($parameter, $value)
    {
        // cleanup n' set query parameter
        if (is_array($value)) {
            foreach ($value as $k => $v) {
                if (self::$_autoEncode) {
                    $v = urlencode(urldecode($v));
                }
                $value[$k] = $v;
            }
        } else {
            if (self::$_autoEncode) {
                $value = urlencode(urldecode($value));
            }
        }
        $this->_queryParams[$parameter] = $value;

        // update query
        $this->_urlComp['query'] = http_build_query($this->_queryParams, '', self::$_argSeparator);
    }


    /**
     * Returns either all query parameter as a assoziative array, or the value of a specific query
     * parameter.
     *
     * @param   string|null  $parameter  Name of parameter or null to get all parameter
     * @param   string|null  $default    Default value to return, in conjunction with passed $parameter
     * @return  string|array|null  Depends of incomming parameter
     */
    public function getQueryParameter($parameter=null, $default=null)
    {
        if ($parameter == null) {
            return $this->_queryParams;
        }

        if (isset($this->_queryParams[$parameter])) {
            return $this->_queryParams[$parameter];
        } else {
            return $default;
        }
    }


    /**
     * Removes existing query parameter.
     *
     * @param   string  $parameter  Name of query parameter to remove
     * @return  void
     */
    public function removeQueryParameter($parameter)
    {
        // unset existing query parameter
        if (isset($this->_queryParams[$parameter])) {
            unset($this->_queryParams[$parameter]);
            // update query
            $this->_urlComp['query'] = http_build_query($this->_queryParams, '', self::$_argSeparator);
        }
    }


    /**
     * Sets the fragment.
     *
     * @param   string  $fragment
     * @return  void
     */
    public function setFragment($fragment)
    {
        if (self::$_autoEncode) {
            $fragment = urlencode(urldecode($fragment));
        }
        $this->_urlComp['fragment'] = $fragment;
    }


    /**
     * Returns the fragment, if exists.
     *
     * @return  string|null
     */
    public function getFragment()
    {
        return (isset($this->_urlComp['fragment'])) ? $this->_urlComp['fragment'] : null;
    }


    /**
     * Builds and returns the Url.
     *
     * @param  string
     */
    public function build()
    {
        return self::buildByComponents($this->_urlComp);
    }


    /**
     * Build a url using passed components array.
     *
     * @param   array   $comp  Assoziative array created by parse_url()
     * @return  string  The composed Url
     */
    public static function buildByComponents(array $comp)
    {
        $url = ((isset($comp['scheme']) && !empty($comp['scheme'])) ? $comp['scheme'] . '://' : '') .
               ((isset($comp['user']) && !empty($comp['user'])) ? $comp['user'] . ':' : '') .
               ((isset($comp['pass']) && !empty($comp['pass'])) ? $comp['pass'] . '@' : '') .
               ((isset($comp['host']) && !empty($comp['host'])) ? $comp['host'] : '') .
               ((isset($comp['port']) && !empty($comp['port'])) ? ':' . $comp['port'] : '') .
               ((isset($comp['path']) && !empty($comp['path'])) ? $comp['path'] : '') .
               ((isset($comp['query']) && !empty($comp['query'])) ? '?' . $comp['query'] : '') .
               ((isset($comp['fragment']) && !empty($comp['fragment'])) ? '#' . $comp['fragment'] : '');
        return $url;
    }


    /**
     * Removes beginning and ending path delemiter from path and returns it back. Strips also 
     * whitespache character from beginning and end of a path.
     *
     * @param   string  $path
     * @return  string
     */
    public static function trimPath($path)
    {
        return trim(trim($path), '/');
    }
}
